Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import { NextRequest, NextResponse } from 'next/server';
import { hasAuth, requireAuthEnabled, validateUpstreamUrl } from '../proxyUtils';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
/**
* Subtitle proxy and converter API route.
*
* IMPORTANT: This API route requires a Node.js server runtime to proxy and convert subtitle files.
* The 'force-dynamic' export above ensures this route runs at runtime.
*/
// Convert SRT format to WebVTT format
function srtToWebVtt(srtContent: string): string {
// Add WebVTT header
let webvtt = 'WEBVTT\n\n';
// Split into subtitle blocks
const blocks = srtContent.trim().split('\n\n');
for (const block of blocks) {
const lines = block.trim().split('\n');
if (lines.length < 3) continue;
// Skip the sequence number (first line)
const timeLine = lines[1];
const textLines = lines.slice(2);
// Convert SRT timestamp format to WebVTT format
// SRT: 00:01:24,667 --> 00:01:26,043
// WebVTT: 00:01:24.667 --> 00:01:26.043
const webvttTimeLine = timeLine.replace(/,/g, '.');
// Add cue to WebVTT
webvtt += `${webvttTimeLine}\n`;
webvtt += `${textLines.join('\n')}\n\n`;
}
return webvtt;
}
export async function GET(request: NextRequest) {
try {
const url = request.nextUrl.searchParams.get('url');
if (!url) {
return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 });
}
if (requireAuthEnabled() && !hasAuth(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Allow same-origin /api/* paths (used for internal subtitle endpoints behind rewrites),
// but block arbitrary relative URLs. External URLs must pass allowlist validation.
let upstreamUrl: URL;
if (url.startsWith('/')) {
if (!url.startsWith('/api/')) {
return NextResponse.json({ error: 'Only /api/* relative paths are allowed' }, { status: 400 });
}
upstreamUrl = new URL(`${request.nextUrl.protocol}//${request.nextUrl.host}${url}`);
} else {
const validated = validateUpstreamUrl(url);
if (!validated.ok) {
return NextResponse.json({ error: validated.error }, { status: validated.status });
}
upstreamUrl = validated.url;
}
// Fetch the original subtitle file
const response = await fetch(upstreamUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
// Forward authorization if needed
...(request.headers.get('cookie') ? { 'Cookie': request.headers.get('cookie')! } : {})}});
if (!response.ok) {
console.error('🎬 Failed to fetch subtitle:', response.status, response.statusText);
return NextResponse.json({ error: 'Failed to fetch subtitle' }, { status: response.status });
}
const contentType = response.headers.get('content-type') || '';
const content = await response.text();
let finalContent = content;
const finalContentType = 'text/vtt; charset=utf-8';
// Convert SRT to WebVTT if needed
if (contentType.includes('subrip') || contentType.includes('srt') ||
content.includes('-->') && !content.startsWith('WEBVTT')) {
finalContent = srtToWebVtt(content);
} else if (contentType.includes('vtt') || content.startsWith('WEBVTT')) {
} else {
finalContent = srtToWebVtt(content);
}
// Return with proper headers for subtitle consumption
return new NextResponse(finalContent, {
headers: {
'Content-Type': finalContentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'public, max-age=3600'}});
} catch (error) {
console.error('🎬 Subtitle proxy error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'}});
}
|